Img_topBanner
AWS Developer - 3. Building Your First Serverless API

Updated at : Tue Oct 20 2026

이 포스팅은 AWS Skill Builder의 AWS Developer 과정을 학습하며 남긴 기록입니다.


image

이 단락에서는 Severless API를 구성하는 핵심 요소인 API Gateway와 Lambda의 기본 개념 및 사용방안에 대해 안내한다. 또한 REST API 설계를 지키는 실질적인 방법에 대한 지침도 포함한다.


API Gateway

AWS의 표현을 빌리자면, API Gateway는 안내 데스크 직원이라고 생각하면 된다고 한다. 꽤나 적절한 표현인 것 같다.

Think of API Gateway as your store's front desk. It greets customers, checks their credentials, validates their requests, and then directs them to the right department (Lambda function) to fulfill their needs.


Request transformation flow

API Gateway를 통해 Lambda를 호출하는 요청들은 다음과 같은 과정을 거치게 된다.

  1. Authentication: Validates API keys, IAM roles, or custom authorizers
  2. Request validation: Checks required parameters and request format
  3. Rate limiting: Applies throttling rules to manage request volume
  4. Request transformation: Maps HTTP request to Lambda event format
  5. Lambda invocation: Calls your function with the transformed event

Lambda proxy integration

API Gateway에서 method 구성시, 람다 프록시 통합 기능의 사용 여부를 선택할 수 있다. 프록시 통합 기능을 사용하게 되면, 모든 HTTP request 정보가 람다에게 전달되게 된다. 이로 인해 람다에서 httpMethod, headers 등의 정보들을 다룰 수 있게 된다. 또한, response 형식도 모두 통제할 수 있게 된다. 이 경우 reponse로 잘못된 형식을 반환한다면 API Gateway에서 malformed 오류를 람다에 반환하게 된다.

반면 프록시 통합 기능을 사용하지 않는 경우, API Gateway는 request를 가공하여 람다에게 필요한(설정해 놓은) 데이터만 전달한다. 람다 역시 응답으로 데이터만을 전달하게 되고, API Gateway가 이를 받아 response 형식을 구성하여 응답을 반환하게 된다.

여기까지만 들으면 프록시 통합 기능을 사용하지 않는게 좋다고 생각할 수도 있으나, AWS 추천은 물론 내 경험상으로도, 프록시 통합 기능을 사용하는 것이 훨씬 직관적이며 편리하다. 단순한 응답은 편리할 수도 있으나 응답의 형태를 원하는 대로 변경하려면 이를 위한 API Gateway 구성 방법에 대해 알아야 하며, CORS 설정 등의 요소도 API Gateway 설정을 통해 달성해야 한다. 또한 request 정보를 읽어, 어디서 들어온 정보인지, 누가 보낸 정보인지 등에 대해 확인하거나 및 기록을 남기는 등의 목적을 달성하려면 오히려 더 복잡성이 올라간다고 볼 수도 있다. 따라서 대부분의 상황에서, 프록시 통합을 사용하는 편이 유리하다고 생각한다.


Lambda

람다 함수는 severless API에서 실제 연산을 담당한다. API Gateway는 URL 경로와 메서드를 기반하여 HTTP 요청을 적절한 람다 함수로 라우팅하게 된다.

handler

def handler(event, context):
    # Process the request
    return response

위의 형태는 python runtime의 기본적인 handler 함수이다. Node.js 및 다른 환경도 문법만 다르지 똑같다. 이 함수명은 람다함수의 진입점으로 설정된 이름이기에 굳이 변경하려면 런타임 설정에서의 변경이 우선되어야 한다.

  • event: Contains the HTTP request data from API Gateway
  • context: Provides runtime information (function name, memory limit, remaining time)

위 파라미터들로 정보가 들어오게 되며, 이를 이용해 path parameter, query parameter, body 등의 정보를 읽어와 필요한 연산을 진행하게 된다.


Returning HTTP responses

람다 프록시 통합 기능을 사용한 경우, 람다는 반드시 statusCode, headers, body가 포함된 오브젝트를 반환해야 한다. 아래의 예시를 참조하자.

import json

def create_response(status_code, body):
    return {
        "statusCode": status_code,
        "headers": {
            "Content-Type": "application/json",
            "Access-Control-Allow-Origin": "*",
            "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
            "Access-Control-Allow-Headers": "Content-Type"
        },
        "body": json.dumps(body)
    }

def handler(event, context):
    product = {"id": "prod_123", "name": "Wireless Headphones"}
    return create_response(200, product)

Handling errors

람다 프록시 통합 기능을 사용하면, 응답을 구성하는 권한/책임은 람다(=개발자)에게 있다. 따라서 유의미한 HTTP 상태 코드 및 오류 메세지를 전달하는 것은 매우 중요하다. 이를 위해 일반적으로 try catch 구문을 많이 활용한다. 아래의 예시를 참조하자.

def handler(event, context):
    try:
        product_id = event.get('pathParameters', {}).get('id')
        
        if not product_id:
            return create_response(400, {"error": "Product ID required"})
        
        product = get_product(product_id)
        if not product:
            return create_response(404, {"error": "Product not found"})
        
        return create_response(200, product)
        
    except json.JSONDecodeError:
        return create_response(400, {"error": "Invalid JSON"})
    except Exception as e:
        print(f"Error: {str(e)}")  # Log for debugging
        return create_response(500, {"error": "Internal server error"})

Execution lifecycle

람다는 일반적인 서버(EC2)처럼 항시 준비되어 있는 것이 아니며, 실행 및 종료를 반복한다. 그 생명주기는 3단계로 구분할 수 있다.

  1. Init phase
  • Downloads your code from Amazon Simple Storage Service (Amazon S3)
  • Creates a container with your runtime (Python, Node.js, etc.)
  • Runs code outside your handler function
  • Loads imported modules

람다가 새로운 환경에서 생성되어 위와 같은 작업을 진행하게 된다. 이 단계는 생명주기 동안 한번만 진행되며, 이 단계에서 오래걸리는 작업들을 진행해놓는 것이 좋다.


  1. Invoke phase
  • Receives the event object
  • Executes your handler function
  • Returns the response

이 단계는 매 호출마다 실행되며, handler함수에 작성한 코드를 실행하는 단계이다. Init phase에서 구성한 자원들이 계속 유지되어, 이 시점에서도 사용가능하다.


  1. Shutdown phase

생성된 컨테이너가 일정시간 유휴(Idle)상태를 유지하면 종료되게 된다. 유휴 시간이 얼마나 되는지는 AWS에서도 말해주지 않는다. 그러나 경험적으로 알려지기를 약 10~15분이라고 한다. 다만, 어디까지나 경험적인 데이터이니 신뢰해서는 안된다.


Cold starts & Warm starts

람다 생명주기의 Init phase 실행여부가 Cold starts와 Warm starts를 나누는 기준점이다. Cold starts는 최초 호출되어 Init phase를 거치는 경우를 말하고, Warm starts는 호출된 후 아직 종료되지 않은 시점에 재호출되어 Invoke phase로 바로 진입하는 경우를 말한다.

이 특성을 활용해서, 여러 리소스들을 준비해야 해서 Init phase의 작업이 오래걸리는 경우에는 항시 Warm starts가 되도록 설계할 수도 있다. 이를 위해 일반적으로 사용되는 방법으로 EventBridge Schedule 설정을 통해 일정시간마다 람다가 호출하는 방법과 람다의 provision concurrency 기능을 이용해 일정 수 이상의 람다가 항시 준비되어있도록 하는 방법이 있다.

이 방법들을 활용하면 람다가 언제나 준비된 상태가 되며, 사실상 지연율이 없는 EC2와 같은 성능을 낼 수도 있다. 그러나 이를 활용할 경우, 람다 사용에 있어 높은 이유 중 하나인 비용문제를 다시 고민해볼 필요가 있을 것이다. 람다는 사용한 만큼 지불해야 하는 모델을 사용하는 만큼 잘못된 이용은 예상 못한 청구서를 직면하게 만들 것이다.

개인적인 Warm starts 활용 팁을 남겨보고자 한다. 애초에 Warm starts 전략이 필요한 경우는 호출이 빈번치 않고, 시점이 예측되지 않을 때 사용하는 것이다. 빈번히 호출된다면 람다는 알아서 살아있을테니 말이다. 그렇다면 빈번히 일어나지 않는 작업이 발생할 것 같은 경우를 예측하면 되지 않을까? 예를 들어 어떤 작업을 진행하기 위해서 특정 페이지에 접속해야 한다면, 그 페이지에 접속하는 순간 더미 요청을 한번 보내서 람다를 깨워놓는 식으로 말이다.


CORS configuration

CORS(Cross-Origin Resource Sharing)는 브라우저가 사용하는 정책으로, API가 CORS 헤더를 작성해 명시적으로 허용해놓지 않는 한, 다른 도메인 간의 요청을 차단하는 정책이다.

브라우저가 이를 검증하는 흐름은 다음과 같다.

  1. Browser sends OPTIONS request(preflight) to check permissions
  2. Your API returns CORS headers indicating what's accepted
  3. Browser proceeds with the actual request if permissions match
  4. If permissions don't match, browser blocks the request

위에서 나타나듯, 브라우저는 요청을 보내고 받아 검증을 진행한다. 이는 보안을 위해 브라우저가 수행하는 기본 동작이다. 따라서 MS나 Google 같은 곳에 들어가 브라우저 자체를 뜯어고치는게 아닌한 손댈 수 없다. 아닌가. W3C에 들어가 규칙을 바꿔야 가능한건가..

따라서 우리는 CORS 문제가 발생하지 않도록, 2번째 단계를 충족시켜야 한다. 서버에서 올바른 CORS headers를 갖고 응답을 보내도록 만들어야 하는 것이다. 이를 위해 다음의 2가지를 충족시키면 된다.

  1. API Gateway에서 OPTIONS 경로를 만들어 연결한다.
  2. Lambda에서 CORS 헤더를 포함한 응답을 반환한다. (프록시 통합)

경로를 생성하는 것은 어려운 일이 아니니 넘어가도록 하자. CORS 헤더에는 다음과 같은 항목들이 있다.

  • Access-Control-Allow-Origin: 허용되는 도메인주소 (e.g. * (all) or specific domain)
  • Access-Control-Allow-Methods: 허용되는 HTTP method (e.g. GET, POST, PUT, DELETE, OPTIONS)
  • Access-Control-Allow-Headers: 허용되는 Requset 헤더 (e.g. Content-Type, Authorization)

위의 값들을 헤더에 작성하여 응답으로 내보내면 된다. 자세한 사용 예시는 상단의 Returning HTTP responses 파트에 작성되어 있다.

그렇다면 이 CORS 검증은 매 request마다 발생하게 될까? 그렇지 않다. 만일 그렇다면 아무리 preflight가 가벼운 요청이라 하더라도 부담이 될 것이다. 이 검증 요청은 특수한 경우, 즉 서버의 데이터에 변경을 줄 것으로 예상되는 경우에만 진행된다. 그리고 그 경우는 다음과 같다.

  • requests POST, PUT, DELETE or requests with custom headers

REST Design

REST API는 REST(REpresentational State Transfer) 아키텍처 스타일의 설계 원칙을 준수하는 API를 말한다. 여러 개념 소개 글에서 REST API는 stateless, cacheable 등의 특징을 갖는다고 소개하며, 장황한 소개를 한다. 물론 틀린 말은 아니다.

하지만 결국 문자 그대로 설계 원칙, 즉 규칙이라고 할 수 있다. 약속한 규칙을 잘 지킴을 통해, API가 예측가능하고 소통하기 용이해지도록 만드는 것이 목적인 것이다. 그럼 무엇을 어떻게 만들어야 그 설계 원칙을 지키는 행위가 되는 것일까? 여기서는 그 어떻게에 대해 다룬다.


Resource-based URL design

API 앤드포인트를 구성함에 있어서도 REST API는 작업이 아닌 리소스를 중심으로 엔드포인트를 구성해야 한다. 다음의 내용과 예시들을 참조하자.

  • 동사가 아닌 명사를 사용한다. (e.g. /getProducts X , /products O)
  • 식별자는 경로(path)로 사용한다. (e.g. /products/prod_123)
  • 정보를 걸러내는(filtering) 작업에는 query를 사용한다 (e.g. ?category=Electronics&limit=10)

image


HTTP methods define operations

각각의 HTTP method에는 특정한 목적이 있다. 이를 지켜가며 사용해야 한다.

image

  • Safe: 서버의 데이터를 변경하지 않음
  • Idempotent: 멱등성, 동일한 요청이 여러번 발생해도 동일한 결과가 나옴

HTTP status codes

HTTP 상태코드는 클라이언트에게 요청의 성공여부, 다음에 취해야 할 동작에 대한 지시를 알려주는 중요한 요소이다.

image

람다 프록시 통합으로 개발시, HTTP 상태코드는 람다에서 직접 반환하게 된다. 따라서 정해진 규격을 지키는 것은 개발자의 책임이 되니 주의가 필요하다. 위의 예시 이외에도 많은 HTTP 상태코드가 존재하니 이는 mdn 문서에서 확인하도록 하자.


위와 같은 규칙들이 모두 지켜진 API라면 REST API라 할 수 있다. 그러나 이 규칙이 지켜지지 않더라도 얼마든지 API를 만들 수 있으며, 그것이 더 직관적이거나 편리하다고 느낄 수 있다.

/products와 /getProducts를 비교하면 상품을 가져온다는 뜻이 포함된 후자가 더 직관적으로 무엇을 하려고 하는지 알기 좋다. POST 메소드를 이용해 리소스 생성/업데이트/삭제를 모두 처리할 수도 있다. 그럼 경로도 하나만 만들면 되니 운영도 편해질 것이다.

다만 세상에는 수많은 개발자와 그들이 만든 API들이 존재하기에 이러한 규칙이 없고서는 소통이 너무 힘들어진다. 우리는 모두 사과와 바나나를 구분할 수 있다. 그런데 갑자기 내가 사과와 바나나의 이름을 바꿔 부르기로 결정한다면? 나는 얼마든지 알아듣는다. 다만 남과 소통할 수는 없을 것이다.